package org.eclipse.emf.db.util;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.emf.cdo.server.internal.db.CDODBSchema;
import org.eclipse.emf.cdo.server.internal.db.DBAnnotation;
import org.eclipse.emf.common.util.Enumerator;
import org.eclipse.emf.db.Activator;
import org.eclipse.emf.db.DBObject;
import org.eclipse.emf.db.RemoteException;
import org.eclipse.emf.db.impl.DBObjectImpl;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EEnum;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.ETypedElement;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.extensions.IDBSchema;

import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

public final class DBUtil {

    public static final String INTERNAL_CLASS_SUFFIX="_internal_class"; //$NON-NLS-1$
    public static final String INTERNAL_CLASS_NAME_SUFFIX=INTERNAL_CLASS_SUFFIX + "_name"; //$NON-NLS-1$

    //    private static final SimpleDateFormat MYSQL_DATE_FORMAT=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //$NON-NLS-1$

    public static String getInternalClass(IDBSchema schema, EReference reference) {
        return schema.getColumnName(reference) + INTERNAL_CLASS_SUFFIX;
    }

    private static String getInternalClassName(IDBSchema schema, EReference reference) {
        return schema.getColumnName(reference) + INTERNAL_CLASS_NAME_SUFFIX;
    }

    private static final int INITIAL_CAPACITY=10000;

    private static Cache<Long, DBObject> objects=enableLowMemoryCache(CacheBuilder.newBuilder().initialCapacity(INITIAL_CAPACITY).weakValues()
            .<Long, DBObject> build());

    private static Cache<Long, DBObject> enableLowMemoryCache(Cache<Long, DBObject> cache) {
        if (Boolean.parseBoolean(System.getProperty("dev.dbemf.lowmemorycache"))) { //$NON-NLS-1$
            cache=new LowMemoryCache<Long, DBObject>(cache);
        }
        return cache;
    }

    public static void clearObjectCache() {
        objects.invalidateAll();
    }

    private static ThreadLocal<Boolean> canSave=new ThreadLocal<Boolean>() {
        @Override
        protected Boolean initialValue() {
            return false;
        }
    };

    private static ThreadLocal<List<Runnable>> post=new ThreadLocal<List<Runnable>>() {
        @Override
        protected List<Runnable> initialValue() {
            return Lists.newArrayList();
        }
    };

    private DBUtil() {
    }

    /**
     * *Must* be called after {@link #createOrUpdateDBStructure(Connection, EPackage)}
     */
    public static void migrateFromCDO(IDBSchema schema) throws SQLException {
        migrateFromCDO(schema, null, null);
    }

    private static final int PARTITION_SIZE=1000;

    /**
     * *Must* be called after {@link #createOrUpdateDBStructure(Connection, EPackage)}
     */
    public static void migrateFromCDO(IDBSchema schema, String taskName, IProgressMonitor monitor) throws SQLException {
        Statement stmt=schema.getConnection().createStatement();
        try {
            List<EClass> concreteClasses=DBModelInformationCache.getConcreteClasses(schema.getPackage());
            monitor.beginTask(taskName, concreteClasses.size());
            for (EClass clazz : concreteClasses) {
                stmt.execute("ALTER TABLE " + schema.getTableName(clazz) + " CHANGE COLUMN `cdo_id` `cdo_id` BIGINT(20) NOT NULL AUTO_INCREMENT ;");
                stmt.execute("ALTER TABLE " + schema.getTableName(clazz) + " MODIFY `cdo_container` BIGINT(20) ;");
                stmt.execute("ALTER TABLE " + schema.getTableName(clazz) + " MODIFY `cdo_resource` BIGINT(20) ;");
                for (EReference ref : clazz.getEAllReferences()) {
                    if (DBModelInformationCache.hasInheritance(ref)) {
                        for (EClass clazz2 : concreteClasses) {
                            if( ((EClass)ref.getEType()).isSuperTypeOf(clazz2) && !clazz2.isAbstract() ) {
                                ResultSet resultSet=stmt.executeQuery("SELECT " + CDODBSchema.ATTRIBUTES_ID + " FROM "
 + schema.getTableName(clazz2));
                                try {
                                    if (resultSet.last()) {
                                        List<Long> values=Lists.newArrayListWithCapacity(resultSet.getRow());
                                        resultSet.beforeFirst();
                                        while (resultSet.next()) {
                                            values.add(resultSet.getLong(1));
                                        }
                                        for (List<Long> ids : Iterables.partition(values, PARTITION_SIZE)) {
                                            stmt.execute("UPDATE " + schema.getTableName(clazz) + " SET " + getInternalClass(schema, ref) + '='
                                                    + cdoInternalClass(clazz2) + ',' + getInternalClassName(schema, ref) + '='
                                                    + DBQueryUtil.quote(cdoInternalClassName(clazz2)) + " WHERE " + schema.getColumnName(ref)
                                                    + " IN ("
                                                    + Joiner.on(',').join(ids) + ")");
                                        }
                                    }
                                } finally {
                                    resultSet.close();
                                }
                            }
                        }
                    }
                }
                monitor.worked(1);
            }
            monitor.done();
        } finally {
            stmt.close();
        }
    }

    public static <T extends DBObject> List<T> getAll(IDBSchema schema, Class<T> c, DBQuery query) throws SQLException {
        return getAll(schema, c, query, null, null);
    }

    public static <T extends DBObject> List<T> getAll(IDBSchema schema, Class<T> c, DBQuery query, String taskName, IProgressMonitor monitor)
            throws SQLException {
        Connection connection=schema.getConnection();
        PreparedStatement stmt=connection.prepareStatement(query.getPreparedStatementSQL());
        try {
            Object[] parameters=query.getParameters();
            for(int i=0; i< parameters.length; i++)
                stmt.setObject(i + 1, parameters[i]);

            ResultSet rSet=stmt.executeQuery();
            List<T> result;
            int size;
            if (rSet.getType() == ResultSet.TYPE_FORWARD_ONLY) {
                size=IProgressMonitor.UNKNOWN;
                result=Lists.newArrayList();
            } else {
                if (!rSet.last())
                    return Collections.emptyList();
                size=rSet.getRow();
                result=Lists.newArrayListWithCapacity(size);
                rSet.beforeFirst();
            }
            if (taskName != null && monitor != null)
                monitor.beginTask(taskName, size);

            Map<String, Integer> mapping=null;
            if (size != 1)
                mapping=new HashMap<String, Integer>();

            EPackage pkg=schema.getPackage();
            EClass eClass=DBModelInformationCache.getEClass(pkg, c);
            while (rSet.next()) {
                // EClass eClass=ModelUtil.ECLASSES.get(c);
                long cdoId=rSet.getLong(CDODBSchema.ATTRIBUTES_ID);
                T item=(T) getObjectFromCache(connection, eClass, cdoId);
                if (item == null) {
                    item=(T) pkg.getEFactoryInstance().create(eClass);
                    ((DBObjectImpl) item).setCdoID(cdoId);
                    putObjectInCache(item);
                }

                mapping=fetchWithoutId(schema, rSet, item, mapping);
                result.add(item);
                if (taskName != null && monitor != null)
                    monitor.worked(1);
            }
            if (taskName != null && monitor != null)
                    monitor.done();
            return Collections.unmodifiableList(result);
        } catch (Exception e) {
            e.printStackTrace();
            throw new SQLException(e);
        } finally {
            stmt.close();
        }
    }
    
    @Deprecated
    public static <T extends DBObject> List<T> getAll(IDBSchema schema, Class<T> c, String queryString) throws SQLException {
        return getAll(schema, c, queryString, null, null);
    }

    @Deprecated
    public static <T extends DBObject> List<T> getAll(IDBSchema schema, Class<T> c, String queryString, String taskName, IProgressMonitor monitor)
            throws SQLException {
        Connection connection=schema.getConnection();
        Statement stmt=connection.createStatement();
        try {
            ResultSet rSet=stmt.executeQuery(queryString);
            List<T> result;
            int size;
            if (rSet.getType() == ResultSet.TYPE_FORWARD_ONLY) {
                size=IProgressMonitor.UNKNOWN;
                result=Lists.newArrayList();
            } else {
                if (!rSet.last())
                    return Collections.emptyList();
                size=rSet.getRow();
                result=Lists.newArrayListWithCapacity(size);
                rSet.beforeFirst();
            }
            if (taskName != null && monitor != null)
                monitor.beginTask(taskName, size);

            Map<String, Integer> mapping=null;
            if (size != 1)
                mapping=new HashMap<String, Integer>();

            EPackage pkg=schema.getPackage();
            EClass eClass=DBModelInformationCache.getEClass(pkg, c);
            while (rSet.next()) {
                // EClass eClass=ModelUtil.ECLASSES.get(c);
                long cdoId=rSet.getLong(CDODBSchema.ATTRIBUTES_ID);
                T item=(T) getObjectFromCache(connection, eClass, cdoId);
                if (item == null) {
                    item=(T) pkg.getEFactoryInstance().create(eClass);
                    ((DBObjectImpl) item).setCdoID(cdoId);
                    putObjectInCache(item);
                }

                mapping=fetchWithoutId(schema, rSet, item, mapping);
                result.add(item);
                if (taskName != null && monitor != null)
                    monitor.worked(1);
            }
            if (taskName != null && monitor != null)
                monitor.done();
            return Collections.unmodifiableList(result);
        } finally {
            stmt.close();
        }
    }

    public static <T extends DBObject> T query(IDBSchema schema, long cdoID, Class<T> c) throws SQLException {
        if (cdoID < 1) {
            Activator.log(IStatus.WARNING, "DBUtil.query(..) cdoID < 1 -> return NULL ", null); //$NON-NLS-1$
            return null;
        }
        EPackage pkg=schema.getPackage();
        EClass eClass=DBModelInformationCache.getEClass(pkg, c);
        T result=(T) getObjectFromCache(schema.getConnection(), eClass, cdoID);
        if (result == null) {
            result=(T) pkg.getEFactoryInstance().create(eClass);
            reload(schema, result, cdoID);
            putObjectInCache(result);
        }
        return result;
    }

    public static long key(DBObject obj) {
        return key(obj.eClass(), obj.cdoID());
    }

    private static long key(EClass clazz, long cdoid) {
        return (long) clazz.getClassifierID() & 0xFFF | cdoid << 12;
    }

    private static DBObject getObjectFromCache(Connection connection, EClass eClass, long cdoId) throws SQLException {
        long key=key(eClass, cdoId);
        DBObject result=objects.getIfPresent(key);
        return result;
    }

    private static void putObjectInCache(DBObject dbObject) {
        long key=key(dbObject);
        objects.put(key, dbObject);
    }

    public static void reload(DBObject obj) throws SQLException {
        if (isStoredInDB(obj))
            reload(((DBObjectImpl) obj).dbSchema(), obj, obj.cdoID());
        // else NO-OP
    }

    public static void reload(DBObject obj, EReference ref) throws SQLException {
        if (obj != null) {
            EReference opposite=ref.getEOpposite();
            if (ref.getUpperBound() == ETypedElement.UNBOUNDED_MULTIPLICITY) {
                List<DBObject> list=(List<DBObject>) obj.eGet(ref);

                for (DBObject object : ImmutableList.<DBObject> copyOf(obj.dbDetached(ref, DBObject.class))) {
                    list.add(object);
                }

                for (DBObjectImpl target : ImmutableList.<DBObjectImpl> copyOf(Iterables.filter(list, DBObjectImpl.class))) {
                    // reload(con, target, opposite);
                    if (target != null && target.ori() != null) {
                        DBObject value=(DBObject) target.ori().get(opposite);
                        target.eSet(opposite, value);
                        // return true;
                    }
                }
            } else {
                DBObjectImpl target=(DBObjectImpl) obj;
                if (opposite != null && opposite.isContainment()) {
                    reload((DBObject) obj.eGet(ref), opposite);
                }
                if (target.ori() != null) {
                    DBObject value=(DBObject) target.ori().get(ref);
                    obj.eSet(ref, value);
                }
            }
        }
    }

    /* package */static void reload(IDBSchema schema, DBObject obj, long cdoId) throws SQLException {
        synchronized (schema) {
            PreparedStatement stmt=schema.getConnection().prepareStatement(
                    "SELECT * FROM " + schema.getTableName(obj.eClass()) + " WHERE " + CDODBSchema.ATTRIBUTES_ID
                    + " = ?");
            try {
                stmt.setLong(1, cdoId);
                ResultSet rSet=stmt.executeQuery();
                if (rSet.next()) {
                    fetch(schema, rSet, obj, null);
                }
            } finally {
                stmt.close();
            }
        }
    }

    private static Map<String, Integer> fetch(IDBSchema schema, ResultSet rSet, DBObject obj, Map<String, Integer> mapping) throws SQLException {
        ((DBObjectImpl) obj).setCdoID(rSet.getLong(CDODBSchema.ATTRIBUTES_ID));
        return fetchWithoutId(schema, rSet, obj, mapping);
    }

    private static Map<String, Integer> fetchWithoutId(IDBSchema schema, ResultSet rSet, DBObject obj, Map<String, Integer> mapping) throws SQLException {
        ((DBObjectImpl) obj).setRevision(rSet.getLong(CDODBSchema.ATTRIBUTES_CREATED));
        ((DBObjectImpl) obj).setSchema(schema);

        // Copy Attributes
        for (EAttribute att : obj.eClass().getEAllAttributes()) {
            if (att.getUpperBound() != ETypedElement.UNBOUNDED_MULTIPLICITY) {
                String column=schema.getColumnName(att);
                int columnIndex=findColumnIndex(rSet, mapping, column);

                if (columnIndex == -1) {
                    ((DBObjectImpl) obj).internalSetLazy(att);
                } else if (att.getEType().equals(EcorePackage.eINSTANCE.getEString())) {
                    ((DBObjectImpl) obj).internalESet(att, rSet.getString(columnIndex));
                } else if (att.getEType().equals(EcorePackage.eINSTANCE.getEDate())) {
                    Object date=rSet.getObject(columnIndex);
                    if (date instanceof Timestamp) {
                        ((DBObjectImpl) obj).internalESet(att, new java.util.Date(((Timestamp) date).getTime()));
                    } else if (date instanceof java.sql.Date) {
                        ((DBObjectImpl) obj).internalESet(att, new java.util.Date(((java.sql.Date) date).getTime()));
                    } else if (date == null) {
                        ((DBObjectImpl) obj).internalESet(att, null);
                    } else {
                        throw new UnsupportedOperationException();
                    }
                } else if (att.getEType().equals(EcorePackage.eINSTANCE.getEInt()) || att.getEType().equals(EcorePackage.eINSTANCE.getEIntegerObject())) {
                    ((DBObjectImpl) obj).internalESet(att, rSet.getObject(columnIndex));
                } else if (att.getEType().equals(EcorePackage.eINSTANCE.getELong()) || att.getEType().equals(EcorePackage.eINSTANCE.getELongObject())) {
                    ((DBObjectImpl) obj).internalESet(att, rSet.getObject(columnIndex));
                } else if (att.getEType().equals(EcorePackage.eINSTANCE.getEFloat()) || att.getEType().equals(EcorePackage.eINSTANCE.getEFloatObject())) {
                    ((DBObjectImpl) obj).internalESet(att, rSet.getObject(columnIndex));
                } else if (att.getEType().equals(EcorePackage.eINSTANCE.getEDouble()) || att.getEType().equals(EcorePackage.eINSTANCE.getEDoubleObject())) {
                    ((DBObjectImpl) obj).internalESet(att, rSet.getObject(columnIndex));
                } else if (att.getEType().equals(EcorePackage.eINSTANCE.getEBoolean()) || att.getEType().equals(EcorePackage.eINSTANCE.getEBooleanObject())) {
                    ((DBObjectImpl) obj).internalESet(att, rSet.getObject(columnIndex));
                } else if (att.getEType().equals(EcorePackage.eINSTANCE.getEByteArray())) {
                    ((DBObjectImpl) obj).internalESet(att, rSet.getBytes(columnIndex));
                } else if (att.getEType() instanceof EEnum) {
                    try {
                        Method method=att.getEType().getInstanceClass().getMethod("get", int.class); //$NON-NLS-1$
                        ((DBObjectImpl) obj).internalESet(att, method.invoke(null, rSet.getInt(columnIndex)));
                    } catch (Exception e) {
                        Activator.log(IStatus.ERROR, "Failed to get enum value", e); //$NON-NLS-1$
                    }
                } else {
                    throw new UnsupportedOperationException(att.getEType().getName());
                }
            }
        }
        // Copy EReferences
        for (EReference ref : obj.eClass().getEAllReferences()) {
            if (ref.getUpperBound() == 1 /* && !ref.isContainment() */) {
                // 0..1
                int columnIndex=findColumnIndex(rSet, mapping, schema.getColumnName(ref));
                if (columnIndex == -1)
                    throw new UnsupportedOperationException("No lazy value supported for EReference"); //$NON-NLS-1$

                Long cdoId=rSet.getLong(columnIndex);
                if (cdoId == 0L || cdoId == null) {
                    ((DBObjectImpl) obj).map().put(ref, null);
                } else {
                    // LazyLoadingInformation entry;
                    int eClass;
                    if (DBModelInformationCache.hasInheritance(ref)) {
                        columnIndex=findColumnIndex(rSet, mapping, getInternalClass(schema, ref));
                        eClass=rSet.getInt(columnIndex);
                    } else {
                        eClass=DBUtil.cdoInternalClass((EClass) ref.getEType());
                    }

                    ((DBObjectImpl) obj).map().put(ref, new LazyLoadingInformation(cdoId, eClass));
                }
            }
        }

        ((DBObjectImpl) obj).dbClearModified();

        return mapping;
    }

    private static int findColumnIndex(ResultSet rSet, Map<String, Integer> mapping, String column) throws SQLException {
        Integer columnIndex;
        if (mapping == null) {
            try {
                columnIndex=rSet.findColumn(column);
            } catch (SQLException e) {
                columnIndex=-1;
            }
        } else {
            columnIndex=mapping.get(column);
            if (columnIndex == null) {
                try {
                    columnIndex=rSet.findColumn(column);
                } catch (SQLException e) {
                    columnIndex=-1;
                }
                mapping.put(column, columnIndex);
            }
        }
        return columnIndex;
    }

    private static abstract class MyRunnable implements Runnable {
        private final DBObject obj;

        public MyRunnable(DBObject obj) {
            this.obj=obj;
        }

        @Override
        public void run() {
            if (!isStoredInDB(obj))
                throw new ConcurrentModificationException("Object was deleted in the meantime.");
            safeRun(obj);
        }

        protected abstract void safeRun(DBObject obj);
    }

    /* package */static void doSave(IDBSchema schema, final DBObject obj, Collection<EStructuralFeature> features) throws SQLException {
        if (!canSave.get())
            throw new DBException("You must execute this in a transaction");

        PreparedStatement stmt=null;//connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE);
        Connection connection=schema.getConnection();
        ((DBObjectImpl) obj).setSchema(schema);

        Map<String, Object> props=null;
        try {
            //            IDBTypeProvider typeProvider=((DBSchema) schema).getTypeProvider();
            if (obj.cdoID() == -1) {
                props=getValuesAsProperties(schema, obj, obj.eClass().getEAllStructuralFeatures());

                String[] keys=new String[props.size()];
                Object[] values=new Object[props.size()];
                int i=0;
                for (String key : Iterables.filter(props.keySet(), String.class)) {
                    keys[i]=key;
                    values[i]=props.get(key);
                    i++;
                }
                
                StringBuilder valuesBuilder = new StringBuilder();
                for (int q=0; q < values.length; q++)
                    valuesBuilder.append(q==0 ? " ? ":", ? ");
                
                stmt=connection.prepareStatement("INSERT INTO " + schema.getTableName(obj.eClass()) //
                        + " (" + Joiner.on(',').join(keys) + ") VALUES (" + valuesBuilder + ")", Statement.RETURN_GENERATED_KEYS);

                for (int q=0; q < values.length; q++)
                    stmt.setObject(q + 1, values[q]);

                stmt.executeUpdate();

                ResultSet generatedKeys=stmt.getGeneratedKeys();
                generatedKeys.next();
                long cdoId=generatedKeys.getLong(1);
                ((DBObjectImpl) obj).setCdoID(cdoId);
                generatedKeys.close();

                ((DBObjectImpl) obj).setRevision((Long) props.get(CDODBSchema.ATTRIBUTES_CREATED));
                putObjectInCache(obj);

                post.get().add(new MyRunnable(obj) {
                    @Override
                    protected void safeRun(DBObject obj) {
                        fireCreated(obj);
                    }
                });
                // Assume that all 0..* relations are empty
                // for (EReference ref : obj.eClass().getEReferences()) {
                // if (ref.getUpperBound() == ETypedElement.UNBOUNDED_MULTIPLICITY)
                // ((DBObjectImpl) obj).map().put(ref, new DBList(ref, obj));
                // }
            } else {
                props=getValuesAsProperties(schema, obj, features);
                StringBuilder builder=new StringBuilder();
                for (String key : Iterables.filter(props.keySet(), String.class)) {
                    if (builder.length() > 0)
                        builder.append(',');
                    builder.append(key).append(" = ? ");// .append(props.getProperty(key));
                }

                stmt=connection.prepareStatement("UPDATE " + schema.getTableName(obj.eClass()) + " SET " + builder + " WHERE "
                        + CDODBSchema.ATTRIBUTES_ID
                        + " = ? " + " AND " + CDODBSchema.ATTRIBUTES_CREATED + " = ? ");

                int q=1;
                for (String key : Iterables.filter(props.keySet(), String.class)) {
                    stmt.setObject(q, props.get(key));
                    q++;
                }
                stmt.setLong(q, obj.cdoID());
                stmt.setObject(q + 1, obj.dbRevision());

                stmt.executeUpdate();

                if (schema.checkModificationTime()) {
                    int nb=stmt.getUpdateCount();
                    // Check if the revision hasn't changed
                    if (nb == 0) {
                        // Try to analyze deeper the problem: was it deleted or saved somewhere else ?
                        Statement stmt2=null;
                        try {
                            stmt2=connection.createStatement();
                            ResultSet rSet=stmt2.executeQuery("SELECT " + CDODBSchema.ATTRIBUTES_CREATED + " FROM " + schema.getTableName(obj.eClass())
                                    + " WHERE " + CDODBSchema.ATTRIBUTES_ID + '=' + obj.cdoID());
                            if (rSet.next())
                                throw new ConcurrentModificationException("Revision is " + rSet.getLong(1) + " instead of " + obj.dbRevision());
                            else
                                throw new ConcurrentModificationException("Object " + obj + " was already deleted");
                        } finally {
                            if (stmt2 != null)
                                stmt2.close();
                        }
                    }
                }

                ((DBObjectImpl) obj).setRevision((Long) props.get(CDODBSchema.ATTRIBUTES_CREATED));

                post.get().add(new MyRunnable(obj) {
                    @Override
                    protected void safeRun(DBObject obj) {
                        fireModified(obj);
                    }
                });
            }
            ((DBObjectImpl) obj).dbClearModified();
            ((DBObjectImpl) obj).dbClearDetached();

        } finally {
            if (stmt != null)
                stmt.close();
        }
    }
    
    private static final List<IDBListener> listeners=Collections.synchronizedList(new ArrayList<IDBListener>());

    public static void addListener(IDBListener listener) {
        listeners.add(listener);
    }

    public static void removeListener(IDBListener listener) {
        listeners.remove(listener);
    }

    private static void fireDeleted(DBObject obj) {
        for (IDBListener listener : listeners) {
            try {
                listener.deleted(obj);
            } catch (Throwable t) {
                Activator.log(IStatus.ERROR, "Internal error while notifying modification", t); //$NON-NLS-1$
            }
        }
    }

    private static void fireModified(DBObject obj) {
        for (IDBListener listener : listeners) {
            try {
                listener.modified(obj);
            } catch (Throwable t) {
                Activator.log(IStatus.ERROR, "Internal error while notifying modification", t); //$NON-NLS-1$
            }
        }
    }

    private static void fireCreated(DBObject obj) {
        for (IDBListener listener : listeners) {
            try {
                listener.created(obj);
            } catch (Throwable t) {
                Activator.log(IStatus.ERROR, "Internal error while notifying creation", t); //$NON-NLS-1$
            }
        }
    }

    private static Map<String, Object> getValuesAsProperties(IDBSchema schema, final DBObject obj, Collection<EStructuralFeature> features) throws SQLException {
        Map<String, Object> values=new HashMap<String, Object>();
        for (EAttribute att : Iterables.filter(features, EAttribute.class)) {
            if (att.getUpperBound() != ETypedElement.UNBOUNDED_MULTIPLICITY) {
                Object value=obj.eGet(att);
                if (att.getEType().equals(EcorePackage.eINSTANCE.getEBoolean()) && value == null)
                    value=false;
                else if (value instanceof Enumerator)
                    value=((Enumerator) value).getValue();
                else if (att.getEType().equals(EcorePackage.eINSTANCE.getEDate())) {
                    String type=EcoreUtil.getAnnotation(att, DBAnnotation.SOURCE_URI, DBAnnotation.COLUMN_TYPE);
                    if (type == null || type.equalsIgnoreCase("TIMESTAMP")) //$NON-NLS-1$
                        value=value == null ? null : new Timestamp(((java.util.Date) value).getTime());
                    else
                        value=value == null ? null : new java.sql.Date(((java.util.Date) value).getTime());
                }
                values.put(schema.getColumnName(att), value);
            }
        }
        for (EReference ref : Iterables.filter(features, EReference.class)) {
            // EReference opposite=ref.getEOpposite();
            if (ref.getUpperBound() != ETypedElement.UNBOUNDED_MULTIPLICITY) {
                DBObject value=(DBObject) obj.eGet(ref);
                values.put(schema.getColumnName(ref), value == null ? null : value.cdoID());
                if (DBModelInformationCache.hasInheritance(ref) && ref != obj.eContainmentFeature()) {
                    values.put(getInternalClass(schema, ref), value == null ? null : cdoInternalClass(value.eClass()));
                    values.put(getInternalClassName(schema, ref), value == null ? null : cdoInternalClassName(value.eClass()));
                }
            }
        }
        values.put(CDODBSchema.ATTRIBUTES_CREATED, System.currentTimeMillis());
        if (obj.eContainer() != null) {
            DBObject eContainer=(DBObject) obj.eContainer();
            Assert.isTrue(DBUtil.isStoredInDB(eContainer), "You must commit parent before");
            values.put(CDODBSchema.ATTRIBUTES_CONTAINER, eContainer.cdoID());

            // TODO CME et QLE
            List<EReference> references=Lists.newArrayList(Collections2.filter(eContainer.eClass().getEAllReferences(), new Predicate<EReference>() {
                @Override
                public boolean apply(EReference ref) {
                    // FIXME Use hasInheritence
                    return ref.getEType().equals(obj.eClass()) && ref.getEContainingClass().isAbstract();
                }
            }));

            if (!references.isEmpty()) {
                EReference eOpposite=references.get(0).getEOpposite();
                DBObjectImpl container=(DBObjectImpl) obj.eContainer();
                values.put(getInternalClass(schema, eOpposite), cdoInternalClass(container.eClass()));
                values.put(getInternalClassName(schema, eOpposite), cdoInternalClassName(container.eClass()));
            }
        }
        return values;
    }

    private static String cdoInternalClassName(EClass eClass) {
        return eClass.getName();
    }

    public static int cdoInternalClass(EClass eClass) {
        return eClass.getName().hashCode();
    }

    /* package */static void delete(IDBSchema schema, DBObject obj) throws SQLException {
        PreparedStatement stmt=schema.getConnection().prepareStatement(
                "DELETE FROM " + schema.getTableName(obj.eClass()) + " WHERE " + CDODBSchema.ATTRIBUTES_ID
                + " = ?");
        try {
            stmt.setLong(1, obj.cdoID());
            stmt.execute();
            // stmt.execute("DELETE FROM " + DBQueryUtil.getTableName(obj.eClass()) + " WHERE " + CDODBSchema.ATTRIBUTES_ID + '=' + obj.cdoID());

            // On supprime les enfants
            if (!obj.eClass().getEAllContainments().isEmpty()) {
                for (EReference reference : obj.eClass().getEAllContainments()) {
                    if (reference.getUpperBound() == 1) {
                        if(obj.eGet(reference) != null)
                            delete(schema, (DBObject) obj.eGet(reference));
                    } else {
                        for (DBObject child : (List<DBObject>) obj.eGet(reference)) {
                            delete(schema, child);
                        }
                    }
                }
            }

            // On informe le parent
            // FIXME CME awfull
            if (obj.eContainmentFeature() instanceof EReference) {
                // obj.eSet(obj.eContainmentFeature(), null);
                EReference reference=obj.eContainmentFeature();
                if (reference.getUpperBound() == 1 && reference.getEOpposite() != null && obj.eContainer() != null) {
                    DBObjectImpl container=(DBObjectImpl) obj.eContainer();
                    container.map().remove(reference.getEOpposite());
                }
            }

            fireDeleted(obj);
            // On desactive l objet
            objects.invalidate(key(obj));
            ((DBObjectImpl) obj).setCdoID(-1);
            ((DBObjectImpl) obj).setSchema(null);
            // ((DBObjectImpl) obj).map().clear();
            // ((DBObjectImpl) obj).cdoSetResource(null);
            ((DBObjectImpl) obj).setRevision(-1L);

            schema.getConnection().commit();
        } finally {
            stmt.close();
        }
        
    }

    // TODO rename this method 
    public static void safeExecute(IDBSchema schema, DBRunnable runnable) {
        try {
            execute(schema, runnable);
        } catch (SQLException e) {
            Activator.log(IStatus.ERROR, "SQL error", e); //$NON-NLS-1$
            throw new RemoteException(e);
        }
    }

    public static void execute(IDBSchema schema, DBRunnable runnable) throws SQLException {
        Connection connection=schema.getConnection();
        try {
            synchronized (schema) {
        		canSave.set(true);
                runnable.schema=schema;
                runnable.run();
        		connection.commit();
			}
        } catch (RuntimeException t) {
            connection.rollback();
            throw t;
        } catch (SQLException t) {
            connection.rollback();
            throw t;
        } finally {
        	for (Iterator<Runnable> iterator=post.get().iterator(); iterator.hasNext();) {
        		try {
        			Runnable next=iterator.next();
                    next.run();
        		} catch(Throwable t) {
                    Activator.log(IStatus.ERROR, "Internal error", t); //$NON-NLS-1$
        		}
        		iterator.remove();
        	}
            canSave.set(false);
        }
    }

    public static boolean equals(DBObject obj1, DBObject obj2) {
        if (obj1 == null || obj2 == null)
            return false;

        if (DBUtil.isStoredInMemory(obj1))
            return obj1 == obj2;

        if (obj1.eClass().getClassifierID() != obj2.eClass().getClassifierID())
            return false;

        if (obj1.cdoID() != ((DBObjectImpl) obj2).cdoID())
            return false;

        return true;
    }

    public static boolean isStoredInDB(DBObject object) {
        return object.cdoID() != -1;
    }

    public static boolean isStoredInMemory(DBObject object) {
        return !isStoredInDB(object);
    }

    //    public static String internalClass(EReference ref) {
    //        return INTERNAL_CLASS.apply(ref);
    //    }

    public static DBObject getInCache(long value) {
        return objects.getIfPresent(value);
    }

    //    public static String[] listColumns(Connection con, String tableName) throws SQLException {
    //        Statement statement=null;
    //        ResultSet rSet=null;
    //        try {
    //            statement=con.createStatement();
    //            List<String> result=Lists.newArrayList();
    //            if (isMSSQLServer(con)) {
    //                ResultSet resultSet=statement.executeQuery("exec sp_columns " + tableName); //$NON-NLS-1$
    //                while (resultSet.next()) {
    //                    String columnName=resultSet.getString("Field"); //$NON-NLS-1$
    //                    result.add(columnName);
    //                }
    //            } else {
    //                ResultSet resultSet=statement.executeQuery("DESCRIBE " + tableName); //$NON-NLS-1$
    //                while (resultSet.next()) {
    //                    String columnName=resultSet.getString("Field"); //$NON-NLS-1$
    //                    result.add(columnName);
    //                }
    //            }
    //            return result.toArray(new String[result.size()]);
    //        } finally {
    //            if (rSet != null)
    //                rSet.close();
    //            if (statement != null)
    //                statement.close();
    //        }
    //    }

    //    public static void createEmptyTable(Connection connection, String tableName) throws SQLException {
    //        Statement statement=null;
    //        try {
    //            statement=connection.createStatement();
    //            String createTableQuery;
    //            if (isMSSQLServer(connection)) {
    //                createTableQuery=MessageFormat.format(
    //                        "CREATE TABLE {0} (cdo_id int NOT NULL IDENTITY PRIMARY KEY, cdo_created bigint NOT NULL, cdo_container int)", tableName); //$NON-NLS-1$
    //            } else {
    //                createTableQuery=MessageFormat
    //                        .format("CREATE TABLE `{0}` (`cdo_id` bigint(20) NOT NULL AUTO_INCREMENT,`cdo_created` bigint(20) NOT NULL, `cdo_container` bigint(20), UNIQUE KEY `{0}_idx0` (`cdo_id`,`cdo_version`),  KEY `{0}_idx1` (`cdo_id`,`cdo_revised`), KEY `{0}_idx2` (`cdo_container`,`cdo_version`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;", tableName); //$NON-NLS-1$
    //            }
    //            statement.execute(createTableQuery);
    //        } finally {
    //            if (statement != null)
    //                statement.close();
    //        }
    //    }

    //    private static void addColumn(Connection connection, String tableName, String columnName, String type) throws SQLException {
    //        Statement statement=null;
    //        try {
    //            statement=connection.createStatement();
    //            String createColumnQuery;
    //            if (isMSSQLServer(connection)) {
    //                createColumnQuery="ALTER TABLE " + tableName + " ADD " + columnName + ' ' + type; //$NON-NLS-1$ //$NON-NLS-2$ 
    //            } else {
    //                createColumnQuery="ALTER TABLE " + tableName + " ADD COLUMN `" + columnName + "` " + type; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 
    //            }
    //            statement.execute(createColumnQuery);
    //        } finally {
    //            if (statement != null)
    //                statement.close();
    //        }
    //    }

    //    public static String[] listTables(Connection con) throws SQLException {
    //        Statement statement=null;
    //        ResultSet rSet=null;
    //        try {
    //            statement=con.createStatement();
    //            String sql;
    //            if (isMSSQLServer(con)) {
    //                sql="SELECT * FROM sys.Tables"; //$NON-NLS-1$
    //            } else {
    //                sql="SHOW TABLES"; //$NON-NLS-1$
    //            }
    //            rSet=statement.executeQuery(sql);
    //            List<String> result=Lists.newArrayList();
    //            while (rSet.next()) {
    //                result.add(rSet.getString(1));
    //            }
    //            return result.toArray(new String[result.size()]);
    //        } finally {
    //            if (rSet != null)
    //                rSet.close();
    //            if (statement != null)
    //                statement.close();
    //        }
    //    }
    //
    //    private static boolean isMSSQLServer(Connection con) {
    //        Class<? extends Connection> clazz=con.getClass();
    //        return clazz != null && clazz.getName().startsWith("com.microsoft");
    //    }

    public static Exception close(Statement statement) {
        if (statement != null) {
            try {
                statement.close();
            } catch (Exception ex) {
                Activator.log(IStatus.ERROR, "Failed closing Statement", ex); //$NON-NLS-1$
                return ex;
            }
        }

        return null;
    }

    public static Exception close(ResultSet resultSet) {
        if (resultSet != null) {
            try {
                Statement statement=resultSet.getStatement();
                if (statement != null && statement.getMaxRows() != 0) {
                    statement.setMaxRows(0);
                }
            } catch (Exception ignore) {
            }

            try {
                resultSet.close();
            } catch (Exception ex) {
                Activator.log(IStatus.ERROR, "Failed closing ResultSet", ex); //$NON-NLS-1$
                return ex;
            }
        }

        return null;
    }
}